import pty
import os
import signal
import sys
import termios
import select
import multiprocessing
import traceback
import struct
if sys.platform == 'linux':
    shell = '/bin/bash'
elif sys.platform == 'darwin':
    shell = '/bin/zsh'
else:
    raise Exception('Unsupported platform')


pid, fd = (None, None)
out_from_pty = None
_reading_threading = None
_force_break = False
# this pipe just used to break the select.select() in the reading thread
parent_pipe, child_pipe = multiprocessing.Pipe() 

def set_pty_size(rows, cols):
    import fcntl
    if not fd:
        return
    size = struct.pack("HHHH", rows, cols, 0, 0)
    fcntl.ioctl(fd, termios.TIOCSWINSZ, size)

def set_pty_echo(using_echo:bool):
    global fd
    if not fd:
        return
    # termios.tcflush(fd, termios.TCIFLUSH)
    # attrs = termios.tcgetattr(fd)
    if using_echo:
        input_into_pty('stty echo')
        # attrs[3] |= termios.ECHO  # turn on echo
    else:
        input_into_pty('stty -echo')
        # attrs[3] &= ~termios.ECHO  # turn off echo
    # termios.tcsetattr(fd, termios.TCSANOW, attrs)

def start_pty(out_function):
    global _force_break, _reading_threading, out_from_pty
    if not out_function or _reading_threading:
        return
    import threading
    global pid, fd
    pid, fd = pty.fork()
    if pid == 0:
        os.execlp(shell, shell)
        exit(0)
    attrs = termios.tcgetattr(fd)
    attrs[3] &= ~termios.ECHO
    termios.tcsetattr(fd, termios.TCSANOW, attrs)
    os.read(fd, 1024)
    out_from_pty = out_function
    def _read_from_pty(*args):
        fd, child_pipe = args
        global _force_break, _reading_threading, out_from_pty
        while 1:
            try:
                has_data, _, _ = select.select([fd, child_pipe], [], [])
                if not has_data:
                    break
                if child_pipe in has_data:
                    data = child_pipe.recv()
                    if data == 'close':
                        break
                if _force_break:
                    break
                if fd not in has_data:
                    continue

                out_str = os.read(fd, 4096).decode()
                if not out_str:
                    break
                out_from_pty(out_str)
            except OSError as e:
                if e.errno == 5: # Input/output error, means the fd is closed
                    break
                else:
                    traceback.print_exc()
                    break
            except Exception as e:
                traceback.print_exc()
                break
    _reading_threading = threading.Thread(
        target=_read_from_pty, args=(fd, child_pipe))
    _force_break = False
    _reading_threading.start()


def input_into_pty(input_data: str):
    if not input_data.endswith('\n'):
        input_data = f'{input_data}\n'
    os.write(fd, input_data.encode())


def input_into_pty_no_newline(input_data: str):
    os.write(fd, input_data.encode())


def close_pty():
    global _force_break, _reading_threading
    _force_break = True
    os.kill(pid, signal.SIGTERM)
    os.close(fd)
    parent_pipe.send('close')
    if _reading_threading:
        _reading_threading.join()
        _reading_threading = None


def _check_subprocess():
    global _force_break, _reading_threading
    retcode = os.waitpid(pid, os.WNOHANG)
    if retcode[0] == pid:
        os.close(fd)
        _force_break = True
        if _reading_threading:
            _reading_threading.join()
            _reading_threading = None
        return True
    else:
        return False
